============ Synthèse des résultats de l'analyse ============
import ipywidgets as widgets
from IPython.display import Image
from IPython.core.display import HTML
from IPython.display import display
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
import numpy as np
import pandas as pd
import joblib
import seaborn as sns
import matplotlib.pyplot as plt
# Ling
import spacy
from collections import Counter
import nltk
from nltk.corpus import stopwords
from nltk.tokenize import sent_tokenize, word_tokenize, WordPunctTokenizer, RegexpTokenizer
from nltk import ngrams
from wordcloud import WordCloud
#Gensim
import gensim
import gensim.corpora as corpora
from gensim.utils import simple_preprocess
from gensim.models.coherencemodel import CoherenceModel
from gensim.models.ldamodel import LdaModel
#CV
import cv2
from PIL import Image
from skimage.io import imshow, imread
from skimage.transform import resize
from skimage.color import rgb2gray
# ConvNet
from keras.applications.vgg16 import preprocess_input, decode_predictions, VGG16
from keras.models import Model
from tensorflow.keras.utils import img_to_array, load_img
#Vis
import pyLDAvis
import pyLDAvis.gensim
import plotly.express as px
import plotly.offline as py
Mise en place d'une nouvelle fonctionnalité de collaboration dans le cadre de l'amélioration de la plateforme Avis Restau :
Les fichiers json suivants ont été téléchargés depuis le site Yelp :
Nous avons parcouru les lignes et extrait les informations dont nous avons besoin :
categoriesAu final, nous avons créé le dataframe regroupant toutes les données :
# Chargement du jeu de données
data = joblib.load('df_depart')
data.head()
| business_id | name | address | city | state | postal_code | review_count | is_open | attributes | categories | review_id | user_id | stars | text | date | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 80 | 1 | {'RestaurantsDelivery': 'False', 'OutdoorSeati... | Restaurants, Food, Bubble Tea, Coffee & Tea, B... | BXQcBN0iAi1lAUxibGLFzA | 6_SpY41LIHZuIaiDs5FMKA | 4.0 | This is nice little Chinese bakery in the hear... | 2014-05-26 01:09:53 |
| 1 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 80 | 1 | {'RestaurantsDelivery': 'False', 'OutdoorSeati... | Restaurants, Food, Bubble Tea, Coffee & Tea, B... | uduvUCvi9w3T2bSGivCfXg | tCXElwhzekJEH6QJe3xs7Q | 4.0 | This is the bakery I usually go to in Chinatow... | 2013-10-05 15:19:06 |
| 2 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 80 | 1 | {'RestaurantsDelivery': 'False', 'OutdoorSeati... | Restaurants, Food, Bubble Tea, Coffee & Tea, B... | a0vwPOqDXXZuJkbBW2356g | WqfKtI-aGMmvbA9pPUxNQQ | 5.0 | A delightful find in Chinatown! Very clean, an... | 2013-10-25 01:34:57 |
| 3 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 80 | 1 | {'RestaurantsDelivery': 'False', 'OutdoorSeati... | Restaurants, Food, Bubble Tea, Coffee & Tea, B... | MKNp_CdR2k2202-c8GN5Dw | 3-1va0IQfK-9tUMzfHWfTA | 5.0 | I ordered a graduation cake for my niece and i... | 2018-05-20 17:58:57 |
| 4 | MTSW4McQd7CbVtyjqoe9mw | St Honore Pastries | 935 Race St | Philadelphia | PA | 19107 | 80 | 1 | {'RestaurantsDelivery': 'False', 'OutdoorSeati... | Restaurants, Food, Bubble Tea, Coffee & Tea, B... | D1GisLDPe84Rrk_R4X2brQ | EouCKoDfzaVG0klEgdDvCQ | 4.0 | HK-STYLE MILK TEA: FOUR STARS\n\nNot quite su... | 2013-10-25 02:31:35 |
print("Dimensions du jeu de données :",data.shape)
Dimensions du jeu de données : (72125, 15)
Les commentaires rédigés par des personnes insatisfaites se traduisent par le nombre de "stars" attribuées :
plt.figure(figsize=(14,6))
sns.countplot(x='stars', data=data)
plt.title('Distribution des notes attribuées')
plt.xlabel('Note attribuée')
plt.ylabel('Nombre de clients')
plt.show()
La plupart des notes sont positives. Pour notre analyse, nous avons utilisé les lignes contenant 1 et 2 étoiles, car les avis notés à 1 étoile ne sont pas suffisamment nombreux.
# Chargement du jeu de données
data = joblib.load('df_clean')
print("Dimensions du jeu de données :",data.shape)
Dimensions du jeu de données : (13536, 16)
Le preprocessing du texte se traduit par plusieurs actions : création du corpus, tokenisation, nettoyage et normalisation.
Word Cloud avant le nettoyage
Un coup d'oeil sur le contenu du corpus avant le nettoyage :
corpus = data[['text']].copy()
corpus = corpus.text.to_dict()
corpus_str=str(corpus.values())
wordcloud = WordCloud(random_state=8,
normalize_plurals=False,
width=600, height=300,
max_words=300)
wordcloud.generate(corpus_str)
<wordcloud.wordcloud.WordCloud at 0x7f33579fdeb0>
fig, ax = plt.subplots(1, 1, figsize=(16, 12))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()
Le nuage de mots est incompréhensible.
Nous remarquons la présence de nombreux termes qui n'apportent aucun sens à l'analyse linguistique :
L'objectif est d'identifier les sujets d'insatisfaction. Comme le corpus est constitué d'avis négatifs uniquement, nous pouvons enlever les adjectifs et les adverbes en toute sérénité. Idem pour les déterminants et les prépositions.
Tokénisation
Dans un premier temps une séparation d'éléments d'une phrase en tokens :
corpus_list=list(corpus.values())
sentence = corpus_list[:2]
def sent_to_words(text):
for sentence in text:
yield(gensim.utils.simple_preprocess(str(sentence), deacc=True))
extrait_normalized = list(sent_to_words(sentence))
print(extrait_normalized)
[['wife', 'and', 'have', 'eaten', 'lunch', 'here', 'few', 'times', 'over', 'the', 'past', 'weeks', 'always', 'take', 'out', 'and', 'we', 'have', 'never', 'dined', 'in', 'for', 'the', 'most', 'part', 'the', 'lunches', 'have', 'been', 'ok', 'nothing', 'real', 'special', 'to', 'brag', 'about', 'on', 'the', 'last', 'visit', 'we', 'ordered', 'greek', 'salad', 'and', 'two', 'bowls', 'of', 'chili', 'the', 'bar', 'tender', 'is', 'great', 'lady', 'to', 'work', 'with', 'and', 'pleasant', 'every', 'time', 'we', 'call', 'in', 'an', 'order', 'we', 'are', 'told', 'the', 'estimated', 'time', 'it', 'will', 'take', 'to', 'be', 'ready', 'usually', 'minutes', 'every', 'time', 'we', 'arrive', 'generally', 'between', 'minutes', 'we', 'have', 'to', 'wait', 'an', 'additional', 'minutes', 'this', 'last', 'visit', 'was', 'no', 'different', 'paid', 'the', 'bill', 'about', 'and', 'went', 'back', 'to', 'the', 'office', 'to', 'eat', 'very', 'disappointed', 'in', 'the', 'salad', 'as', 'there', 'was', 'virtually', 'no', 'lettuce', 'and', 'only', 'wedge', 'of', 'tomato', 'the', 'chili', 'was', 'served', 'in', 'ounce', 'styrofoam', 'go', 'cups', 'the', 'chili', 'was', 'luke', 'warm', 'at', 'best', 'by', 'the', 'way', 'the', 'drive', 'takes', 'minutes', 'to', 'get', 'to', 'my', 'office', 'from', 'the', 'retaraunt', 'have', 'found', 'the', 'place', 'an', 'ok', 'place', 'as', 'restaurant', 'but', 'certainly', 'appears', 'to', 'be', 'nothing', 'special'], ['after', 'about', 'minutes', 'of', 'waiting', 'patiently', 'for', 'any', 'form', 'of', 'life', 'to', 'serve', 'us', 'chef', 'came', 'out', 'and', 'asked', 'if', 'we', 'had', 'been', 'served', 'he', 'sent', 'over', 'waitress', 'my', 'boyfriend', 'claims', 'his', 'budweiser', 'didn', 'taste', 'right', 'and', 'his', 'salad', 'had', 'slight', 'brown', 'tint', 'to', 'it', 'and', 'barely', 'any', 'green', 'he', 'got', 'steak', 'and', 'broccoli', 'the', 'steak', 'was', 'tiny', 'and', 'shitty', 'but', 'the', 'broccoli', 'was', 'perfecto', 'got', 'half', 'burnt', 'salmon', 'bacon', 'wrap', 'then', 'went', 'to', 'the', 'bathroom', 'and', 'felt', 'like', 'was', 'peeing', 'outside', 'in', 'the', 'degree', 'weather', 'went', 'back', 'to', 'the', 'table', 'and', 'waited', 'for', 'the', 'waitress', 'for', 'bout', 'mins', 'then', 'took', 'her', 'the', 'credit', 'card', 'explained', 'to', 'the', 'manager', 'how', 'cold', 'it', 'is', 'in', 'the', 'women', 'bathroom', 'he', 'said', 'would', 'you', 'like', 'me', 'to', 'bring', 'in', 'the', 'fire', 'pit', 'from', 'outside', 'and', 'light', 'fire', 'for', 'you', 'while', 'laughing', 'at', 'his', 'own', 'joke', 'another', 'worker', 'sitting', 'next', 'to', 'him', 'said', 'it', 'the', 'same', 'in', 'the', 'men', 'bathroom', 'my', 'dick', 'shrivels', 'up', 'every', 'time', 'go', 'in', 'there', 'this', 'place', 'must', 'be', 'drug', 'front']]
Stopwords
Ensuite, suppression de termes inutiles, qui n'apportent pas de sens :
stop_words = stopwords.words('english')
stop_words.extend([
# Mots inutiles repérés dans le corpus et ajoutés à la liste
'not','ve',"will","la","hi","till","bf","idk","bf","fcc","fi",
# Mots repérés lors du LDA qui n'apportent pas de sens aux topics identifiés
"go","get"])
def remove_stopwords(texts):
return [[word for word in simple_preprocess(str(doc)) if word not in stop_words] for doc in texts]
extrait_no_stopwords = remove_stopwords(extrait_normalized)
print(extrait_no_stopwords)
[['wife', 'eaten', 'lunch', 'times', 'past', 'weeks', 'always', 'take', 'never', 'dined', 'part', 'lunches', 'ok', 'nothing', 'real', 'special', 'brag', 'last', 'visit', 'ordered', 'greek', 'salad', 'two', 'bowls', 'chili', 'bar', 'tender', 'great', 'lady', 'work', 'pleasant', 'every', 'time', 'call', 'order', 'told', 'estimated', 'time', 'take', 'ready', 'usually', 'minutes', 'every', 'time', 'arrive', 'generally', 'minutes', 'wait', 'additional', 'minutes', 'last', 'visit', 'different', 'paid', 'bill', 'went', 'back', 'office', 'eat', 'disappointed', 'salad', 'virtually', 'lettuce', 'wedge', 'tomato', 'chili', 'served', 'ounce', 'styrofoam', 'cups', 'chili', 'luke', 'warm', 'best', 'way', 'drive', 'takes', 'minutes', 'office', 'retaraunt', 'found', 'place', 'ok', 'place', 'restaurant', 'certainly', 'appears', 'nothing', 'special'], ['minutes', 'waiting', 'patiently', 'form', 'life', 'serve', 'us', 'chef', 'came', 'asked', 'served', 'sent', 'waitress', 'boyfriend', 'claims', 'budweiser', 'taste', 'right', 'salad', 'slight', 'brown', 'tint', 'barely', 'green', 'got', 'steak', 'broccoli', 'steak', 'tiny', 'shitty', 'broccoli', 'perfecto', 'got', 'half', 'burnt', 'salmon', 'bacon', 'wrap', 'went', 'bathroom', 'felt', 'like', 'peeing', 'outside', 'degree', 'weather', 'went', 'back', 'table', 'waited', 'waitress', 'bout', 'mins', 'took', 'credit', 'card', 'explained', 'manager', 'cold', 'women', 'bathroom', 'said', 'would', 'like', 'bring', 'fire', 'pit', 'outside', 'light', 'fire', 'laughing', 'joke', 'another', 'worker', 'sitting', 'next', 'said', 'men', 'bathroom', 'dick', 'shrivels', 'every', 'time', 'place', 'must', 'drug', 'front']]
Lemmatisation
Finalement, une lemmatisation - remplacement des tokens par leur forme canonique :
nlp = spacy.load('en_core_web_sm', disable=["parser", "ner"])
def lemmatization(texts, allowed_postags=['NOUN', 'VERB']):
texts_out = []
for sent in texts:
doc = nlp(" ".join(sent))
texts_out.append([token.lemma_ for token in doc if token.pos_ in allowed_postags])
return texts_out
extrait_lemmatized = lemmatization(extrait_no_stopwords)
extrait_final = remove_stopwords(extrait_lemmatized)
print(extrait_final)
[['eat', 'lunch', 'time', 'week', 'take', 'dine', 'part', 'lunch', 'brag', 'visit', 'order', 'greek', 'salad', 'bowl', 'chili', 'bar', 'tender', 'lady', 'work', 'time', 'call', 'order', 'tell', 'estimate', 'time', 'take', 'minute', 'time', 'arrive', 'minute', 'wait', 'minute', 'visit', 'pay', 'bill', 'office', 'eat', 'salad', 'lettuce', 'wedge', 'tomato', 'chili', 'serve', 'ounce', 'styrofoam', 'cup', 'chili', 'way', 'drive', 'take', 'minute', 'office', 'retaraunt', 'find', 'place', 'place', 'restaurant', 'appear'], ['minute', 'wait', 'form', 'life', 'serve', 'chef', 'come', 'ask', 'serve', 'send', 'boyfriend', 'claim', 'budweiser', 'taste', 'salad', 'tint', 'steak', 'broccoli', 'steak', 'broccoli', 'perfecto', 'burn', 'salmon', 'bacon', 'wrap', 'bathroom', 'feel', 'pee', 'degree', 'weather', 'table', 'wait', 'bout', 'min', 'take', 'credit', 'card', 'explain', 'manager', 'woman', 'bathroom', 'say', 'bring', 'fire', 'pit', 'fire', 'laugh', 'joke', 'worker', 'sit', 'say', 'man', 'bathroom', 'shrivel', 'time', 'place', 'drug', 'front']]
Comparaison avant - après
Comparons quelques phrases avant et après le prétraitement :
for old_sen, new_sen in zip(sentence, extrait_lemmatized):
print("Avant : ", old_sen[:100])
print("Après : ", new_sen[:10])
print()
Avant : Wife and I have eaten lunch here a few times over the past 6-weeks. Always a take-out and we have n Après : ['eat', 'lunch', 'time', 'week', 'take', 'dine', 'part', 'lunch', 'brag', 'visit'] Avant : After about 7 minutes of waiting patiently for any form of life to serve us, a chef came out and ask Après : ['minute', 'wait', 'form', 'life', 'serve', 'chef', 'come', 'ask', 'serve', 'send']
data_normalized=joblib.load('normalized_corpus')
wordcloud_text = [x for xs in data_normalized for x in xs]
wc_text = ' '.join(wordcloud_text)
# Instantiate a new wordcloud.
wordcloud = WordCloud(random_state=8,
#collocations=True,
min_word_length=4,
#collocation_threshold=5,
normalize_plurals=False,
width=600, height=300,
max_words=300)
wordcloud.generate(wc_text)
<wordcloud.wordcloud.WordCloud at 0x7f3338858d30>
fig, ax = plt.subplots(1, 1, figsize=(16, 12))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()
Le résultat est très satisfaisant :
Le LDA est une technique de modélisation des documents de texte.
Pour identifier les principaux topics de notre corpus, nous allons devoir passer par plusieurs étapes :
corpora.Dictionary de Gensim)doc2bow). Le résultat est une matrice creuse contenant chaque terme employé et sa fréquence.Dictionnaire & Bag of words
id2word = corpora.Dictionary(data_normalized)
bow_corpus = []
for text in data_normalized:
new = id2word.doc2bow(text) # BAG OF WORDS
bow_corpus.append(new)
print (bow_corpus[:1])
[[(0, 1), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 2), (9, 2), (10, 1)]]
print('Combien de tokens dans le dictionnaire : %d' % len(id2word))
print('Combien de documents dans le BOW : %d' % len(bow_corpus))
Combien de tokens dans le dictionnaire : 8909 Combien de documents dans le BOW : 9946
Coherence score
Le Coherence Score est l'une des principales techniques utilisées pour estimer le nombre de topics. Il évalue un seul topic en mesurant le degré de similitude sémantique entre les mots les mieux notés au sein du topic donné.
def compute_coherence_values(dictionary, corpus, texts, limit, start=2, step=3):
"""
Compute c_v coherence for various number of topics
Parameters:
----------
dictionary : Gensim dictionary
corpus : Gensim corpus
texts : List of input texts
limit : Max num of topics
Returns:
-------
model_list : List of LDA topic models
coherence_values : Coherence values corresponding to the LDA model with respective number of topics
"""
coherence_values = []
model_list = []
for num_topics in range(start, limit, step):
model=LdaModel(corpus=corpus, id2word=dictionary, num_topics=num_topics)
model_list.append(model)
coherencemodel = CoherenceModel(model=model, texts=texts, dictionary=dictionary, coherence='c_v')
coherence_values.append(coherencemodel.get_coherence())
return model_list, coherence_values
model_list, coherence_values = compute_coherence_values(dictionary=id2word, corpus=bow_corpus, texts=data_normalized, start=2, limit=40, step=6)
plt.figure(figsize=(14, 6))
limit = 40
start = 2
step = 6
x = range(start, limit, step)
plt.plot(x, coherence_values)
plt.title("Coherence score pour 40 topics")
plt.xlabel("Nombre de topics")
plt.ylabel("Coherence score")
plt.legend(("coherence_values"), loc='best')
plt.show()
Le graphique ci-dessus montre que le score de cohérence baisse avec le nombre de topics. Il semblerait que le nombre de topics optimal se situe autour de 5.
Affichons les scores exacts pour avoir une idée précise :
for m, cv in zip(x, coherence_values):
print("Nb de topics =", m, ", Coherence Value est de", round(cv, 4))
Nb de topics = 2 , Coherence Value est de 0.4108 Nb de topics = 8 , Coherence Value est de 0.3949 Nb de topics = 14 , Coherence Value est de 0.3717 Nb de topics = 20 , Coherence Value est de 0.4077 Nb de topics = 26 , Coherence Value est de 0.3642 Nb de topics = 32 , Coherence Value est de 0.3643 Nb de topics = 38 , Coherence Value est de 0.3542
Le meilleur score est attribué aux 2 sujets. Dans notre cas, deux sujets d'insatisfaction n'est pas un nombre suffisant. De l'autre côté, huit topics c'est trop, tout en sachant que le Coherence Score continue sa descente entre 7 et 15 topics. Nous allons donc comparer les résultats pour 6, 4 et 8 topics.
lda_model = gensim.models.ldamodel.LdaModel(corpus=bow_corpus,
id2word=id2word,
num_topics=4,
passes=10,
alpha="auto")
pyLDAvis.enable_notebook()
vis = pyLDAvis.gensim.prepare(lda_model, bow_corpus, id2word, mds="mmds", R=30)
vis
/home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/pyLDAvis/_prepare.py:228: FutureWarning: In a future version of pandas all arguments of DataFrame.drop except for the argument 'labels' will be keyword-only /home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses from imp import reload /home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses from imp import reload /home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses from imp import reload /home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/past/builtins/misc.py:45: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses from imp import reload
Poids des topics
from pprint import pprint
pprint(lda_model.print_topics())
[(0, '0.047*"burger" + 0.037*"fry" + 0.012*"cake" + 0.009*"taste" + ' '0.009*"ice_cream" + 0.008*"donut" + 0.007*"bun" + 0.006*"shake" + ' '0.006*"location" + 0.005*"drive"'), (1, '0.083*"order" + 0.039*"pizza" + 0.020*"time" + 0.019*"say" + 0.018*"call" + ' '0.017*"take" + 0.013*"tell" + 0.012*"ask" + 0.010*"give" + 0.010*"wing"'), (2, '0.049*"food" + 0.028*"place" + 0.024*"service" + 0.018*"time" + ' '0.018*"come" + 0.012*"restaurant" + 0.012*"order" + 0.011*"wait" + ' '0.011*"take" + 0.011*"table"'), (3, '0.030*"taste" + 0.018*"chicken" + 0.017*"order" + 0.014*"sauce" + ' '0.013*"food" + 0.012*"flavor" + 0.012*"sandwich" + 0.012*"place" + ' '0.012*"meat" + 0.011*"fry"')]
WordCloud par topic
Les mots les plus exposés sont ceux les plus importants pour un topic donné :
import matplotlib.colors as mcolors
# more colors: 'mcolors.XKCD_COLORS'
cols = [color for name, color in mcolors.TABLEAU_COLORS.items()]
cloud = WordCloud(stopwords=stop_words,
background_color='white',
width=2500,
height=1800,
max_words=10,
colormap='tab10',
color_func=lambda *args, **kwargs: cols[i],
prefer_horizontal=1.0)
topics = lda_model.show_topics(formatted=False)
fig, axes = plt.subplots(2, 2, figsize=(10, 8), sharex=True, sharey=True)
for i, ax in enumerate(axes.flat):
fig.add_subplot(ax)
topic_words = dict(topics[i][1])
cloud.generate_from_frequencies(topic_words, max_font_size=300)
plt.gca().imshow(cloud)
plt.gca().set_title('Topic ' + str(i), fontdict=dict(size=16))
plt.gca().axis('off')
plt.subplots_adjust(wspace=0, hspace=0)
plt.axis('off')
plt.margins(x=0, y=0)
plt.tight_layout()
plt.show()
Combien de documents pour chaque topic
Finalement, nous allons vérifier comment sont répartis les 4 topics dans tout le corpus :
def format_topics_sentences(ldamodel=lda_model, corpus=corpus, texts=data):
# Init output
sent_topics_df = pd.DataFrame()
# Get main topic in each document
for i, row in enumerate(ldamodel[corpus]):
row = sorted(row, key=lambda x: (x[1]), reverse=True)
# Get the Dominant topic, Perc Contribution and Keywords for each document
for j, (topic_num, prop_topic) in enumerate(row):
if j == 0: # => dominant topic
wp = ldamodel.show_topic(topic_num)
topic_keywords = ", ".join([word for word, prop in wp])
sent_topics_df = sent_topics_df.append(pd.Series([int(topic_num), round(prop_topic,4), topic_keywords]), ignore_index=True)
else:
break
sent_topics_df.columns = ['Dominant_Topic', 'Perc_Contribution', 'Topic_Keywords']
# Add original text to the end of the output
contents = pd.Series(texts)
sent_topics_df = pd.concat([sent_topics_df, contents], axis=1)
return(sent_topics_df)
df_topic_sents_keywords = format_topics_sentences(ldamodel=lda_model, corpus=bow_corpus, texts=corpus_list)
# Number of Documents for Each Topic
topic_counts = df_topic_sents_keywords['Dominant_Topic'].value_counts()
# Percentage of Documents for Each Topic
topic_contribution = round(topic_counts/topic_counts.sum(), 4)
# Concatenate Column wise
df_dominant_topics = pd.concat([topic_counts, topic_contribution], axis=1)
# Change Column names
df_dominant_topics.columns = ['Nb Docs', '% Docs']
df_dominant_topics
| Nb Docs | % Docs | |
|---|---|---|
| 2.0 | 7124 | 0.7163 |
| 3.0 | 1629 | 0.1638 |
| 1.0 | 1006 | 0.1011 |
| 0.0 | 187 | 0.0188 |
La représentation en 3 dimensions, avec l'algorithme t-SNE, nous permettra de visualiser nos documents par topic.
# Get topic weights and dominant topics
from sklearn.manifold import TSNE
from bokeh.plotting import figure, output_file, show
from bokeh.models import Label
from bokeh.io import output_notebook
# Get topic weights
topic_weights = []
for i, row_list in enumerate(lda_model[bow_corpus]):
topic_weights.append([w for i, w in row_list])
# Array of topic weights
arr = pd.DataFrame(topic_weights).fillna(0).values
# Dominant topic number in each doc
topic_num = np.argmax(arr, axis=1)
# tSNE Dimension Reduction
tsne_model = TSNE(n_components=3, verbose=1, random_state=0, angle=.99, init='pca')
tsne_lda = tsne_model.fit_transform(arr)
/home/sylwia/.local/lib/python3.9/site-packages/sklearn/manifold/_t_sne.py:790: FutureWarning: The default learning rate in TSNE will change from 200.0 to 'auto' in 1.2.
[t-SNE] Computing 91 nearest neighbors... [t-SNE] Indexed 9946 samples in 0.010s... [t-SNE] Computed neighbors for 9946 samples in 0.199s... [t-SNE] Computed conditional probabilities for sample 1000 / 9946 [t-SNE] Computed conditional probabilities for sample 2000 / 9946 [t-SNE] Computed conditional probabilities for sample 3000 / 9946 [t-SNE] Computed conditional probabilities for sample 4000 / 9946 [t-SNE] Computed conditional probabilities for sample 5000 / 9946 [t-SNE] Computed conditional probabilities for sample 6000 / 9946 [t-SNE] Computed conditional probabilities for sample 7000 / 9946 [t-SNE] Computed conditional probabilities for sample 8000 / 9946 [t-SNE] Computed conditional probabilities for sample 9000 / 9946 [t-SNE] Computed conditional probabilities for sample 9946 / 9946 [t-SNE] Mean sigma: 0.000766
/home/sylwia/.local/lib/python3.9/site-packages/sklearn/manifold/_t_sne.py:982: FutureWarning: The PCA initialization in TSNE will change to have the standard deviation of PC1 equal to 1e-4 in 1.2. This will ensure better convergence.
[t-SNE] KL divergence after 250 iterations with early exaggeration: 63.128693 [t-SNE] KL divergence after 1000 iterations: 0.588001
topic_weight_df = pd.DataFrame(topic_weights).fillna(0)
topic_weight_df['topic_num'] = topic_weight_df.idxmax(axis=1)
df_tsne_lda = pd.DataFrame(tsne_lda[:,0:3], columns=['tsne1', 'tsne2','tsne3'])
df_tsne_lda["topic_num"] = topic_weight_df["topic_num"]
fig = px.scatter_3d(df_tsne_lda, x='tsne1', y='tsne2', z='tsne3', color='topic_num', opacity=0.8)
fig.update_layout(margin=dict(l=0, r=0, b=0, t=0))
Conclusion
Nous avons réussi à visualiser les mots utilisés pour exprimer l’insatisfaction, et par conséquent définir les sources du mécontentement des clients :
Il faudrait bien sûr apporter des améliorations, par exemple peaufiner la sélection des mots-clés. Néanmoins, une première analyse a permis d'identifier les principaux sujets d'insatisfaction.
Vérifions la répartition des labels :
df_photos = joblib.load('data_photos')
plt.figure(figsize=(14,6))
sns.countplot(x='label', data=df_photos)
plt.title('Distribution des labels attribués')
plt.xlabel('Label attribué')
plt.ylabel('Quantité de photos')
plt.show()
df_photos["label"].value_counts(normalize=True)
food 0.574458 inside 0.271285 outside 0.084895 drink 0.062459 menu 0.006903 Name: label, dtype: float64
Les photos les plus nombreuses (plus de 57%) illustrent la nourriture. Le menu a été le moins photographié par les clients (moins de 1%).
# Chargement du fichier
data_photos = joblib.load('df_photo_samples')
Extraction de 200 photos par catégorie :
data_photos.groupby("label").count()
| photo_id | business_id | caption | |
|---|---|---|---|
| label | |||
| drink | 123 | 123 | 123 |
| food | 144 | 144 | 144 |
| inside | 151 | 151 | 151 |
| menu | 56 | 56 | 56 |
| outside | 142 | 142 | 142 |
Affichage d'exemples d'images en fonction du label
from matplotlib.image import imread
PATH = "/home/sylwia/Jupyter/P6_NLP/bdd_yelp/yelp_photos/photos/"
def list_fct(name) :
list_image_name = [data_photos.iloc[i].photo_id for i in range(len(data_photos)) if data_photos["label"][i]==name]
return list_image_name
list_labels = ["food", "drink", "inside", "outside","menu"]
for name in list_labels :
print(name)
for i in range(3):
plt.subplot(130 + 1 + i)
filename = PATH + list_fct(name)[i+10]
image = imread(filename)
plt.imshow(image)
plt.show()
food
drink
inside
outside
menu
Le préprocessing a pour objectif d'améliorer les propriétés d'une image en vue d'un futur entraînement, grâce à tout un ensemble de processus, comme grayscale, equalization, filtrage bruit, contraste, floutage.
Chargement d'une image de test :
def show_image (image, title, cmap_type='gray') :
plt.figure(num=None, figsize=(10, 8), dpi=80)
plt.imshow(image)
plt.axis('off')
plt.title(title)
plt.show()
from PIL import Image
img = Image.open("png/dark.jpg")
show_image(img, title="Photo exemple")
La photo est foncée et floue. Essayons d'afficher la photo sous forme d'histogramme.
Dans le contexte d'une image numérique, il s'agit d'un graphique ou un tracé, ce qui donne une idée globale de la distribution d'intensité d'une image. Le tracé contient des valeurs de pixels (allant de 0 à 255) sur l'axe X et le nombre correspondant de pixels sur l'axe Y.
import cv2
img = cv2.imread('png/dark.jpg')
color = ('b','g','r')
plt.figure(figsize=(12, 8))
for i,col in enumerate(color):
histr = cv2.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr, color = col)
plt.xlim([0,256])
plt.title("Histogramme RGB de la photo exemple")
plt.show()
Le bleu et le vert atteignent plus de 3000 valeurs.
Vérifions la taille de l'image :
print('Dimensions : ', img.shape)
Dimensions : (400, 300, 3)
La définition de notre image est donc de 300 pixels par 400 pixels.
Une image en niveaux de gris (2D greyscale) est très utile pour un traitement ultérieur de la segmentation. Un input RGB doit donc être converti en greyscale.
Regardons comment évolue notre image exemple :
def img_comp(original, filtered, title_1, title2):
fig, (ax1, ax2) = plt.subplots(
ncols=2, figsize=(10, 8), sharex=True, sharey=True)
ax1.imshow(original, cmap=plt.cm.gray)
ax1.set_title(title_1)
ax1.axis('off')
ax2.imshow(filtered, cmap=plt.cm.gray)
ax2.set_title(title2)
ax2.axis('off')
# l’option as_gray=True
#img_Gray = imread('png/dark.jpg', as_gray=True)
# la fonction rgb2gray()
img1_Gray = imread('png/dark.jpg')
img2_Gray = rgb2gray(img1_Gray)
img_comp(img, img2_Gray, "Image d'origine","Niveaux de gris avec 'rgb2gray'")
Noir et blanc
img_black_white = np.where(img2_Gray>84/256, 0, 1)
plt.figure(num=None, figsize=(6, 4), dpi=80)
imshow(img_black_white, cmap=plt.get_cmap('gray'))
plt.show()
/home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/skimage/io/_plugins/matplotlib_plugin.py:150: UserWarning: Low image data range; displaying image with stretched contrast.
L'objectif d'une égalisation est d'améliorer le contraste de l'image : soit lui redonner du peps, soit l'adoucir.
Une technique permettant de réajuster le contraste d'une image est l'égalisation des histogrammes : il s'agit d'harmoniser la distribution des niveaux de gris de l'image, de sorte que chaque niveau de l'histogramme contienne idéalement le même nombre de pixels.
Avant
hist,bins = np.histogram(img.flatten(),256,[0,256])
# cumulative distribution function
cdf = hist.cumsum()
cdf_normalized = cdf * float(hist.max()) / cdf.max()
plt.plot(cdf_normalized, color = 'b')
plt.hist(img.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.legend(('cdf','histogram'), loc = 'upper left')
plt.show()
Après
cdf_m = np.ma.masked_equal(cdf,0)
cdf_m = (cdf_m - cdf_m.min())*255/(cdf_m.max()-cdf_m.min())
cdf = np.ma.filled(cdf_m,0).astype('uint8')
# Génération de la nouvelle image égalisée
img2 = cdf[img]
hist,bins = np.histogram(img2.flatten(),256,[0,256])
cdf = hist.cumsum()
cdf_normalized = cdf * float(hist.max()) / cdf.max()
plt.plot(cdf_normalized, color = 'b')
plt.hist(img2.flatten(),256,[0,256], color = 'r')
plt.xlim([0,256])
plt.legend(('cdf','histogram'), loc = 'upper left')
plt.show()
Vue après égalisation : les pixels sont mieux étendus dans toute la zone entre 0 et 255.
img_comp(img, img2, "Photo originale", "Image égalisée")
Filtrage de bruit
dst = cv2.fastNlMeansDenoisingColored(img2,None,10,10,7,12)
img_comp(img2, dst, "Photo égalisée","Photo débruitée")
SIFT est une méthode qui permet de détecter et identifier les éléments similaires entre différentes images numériques.
L'étape fondamentale de l'algorithme consiste à calculer ce que l'on appelle les « descripteurs SIFT » des images à étudier. Les descripteurs, à leur tour, nous seront utiles pour créer les bag of visual words - un regroupement des descripteurs qui sont proches entre eux.
Pour effectuer une étude de similarité de descripteurs (rapprocher ceux qui se ressemblent), nous allons faire appel au clustering.
Affichage des descripteurs
img = cv2.imread(PATH + data_photos.iloc[1].photo_id)
# Préprocessing d'images : grayscale & égalisation
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
# Création des descripteurs
sift = cv2.SIFT_create()
kp, desc = sift.detectAndCompute(gray, None)
img = cv2.drawKeypoints(gray, kp, img)
img = cv2.drawKeypoints(
gray, kp, img, flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# Affichage
display(Image.fromarray(img, "RGB"))
print("Descripteurs : ", desc.shape)
print()
print(desc)
Descripteurs : (815, 128) [[ 49. 2. 0. ... 0. 0. 0.] [ 5. 47. 45. ... 8. 1. 5.] [138. 120. 2. ... 2. 0. 1.] ... [ 0. 0. 0. ... 20. 11. 3.] [ 15. 20. 15. ... 4. 1. 30.] [ 48. 7. 1. ... 0. 0. 0.]]
Par la suite, nous allons effectuer une PCA, ensuite une réduction de dimension via T-SNE.
# Chargement du fichier
im_features = joblib.load('im_features')
PCA
#Clustering
from sklearn import manifold, decomposition
from sklearn import cluster, metrics
from sklearn.cluster import KMeans
from sklearn import preprocessing
from sklearn.decomposition import PCA
from sklearn.metrics import silhouette_score
print("Dimensions avant PCA : ", im_features.shape)
pca = decomposition.PCA(n_components=0.99)
feat_pca= pca.fit_transform(im_features)
print("Dimensions après PCA : ", feat_pca.shape)
Dimensions avant PCA : (616, 844) Dimensions après PCA : (616, 1)
TSNE
tsne = manifold.TSNE(n_components=2, perplexity=30,
n_iter=2000, init='random', random_state=6)
X_tsne = tsne.fit_transform(feat_pca)
/home/sylwia/.local/lib/python3.9/site-packages/sklearn/manifold/_t_sne.py:790: FutureWarning: The default learning rate in TSNE will change from 200.0 to 'auto' in 1.2.
# Création d'un nouveau dataframe
df_tsne = pd.DataFrame(X_tsne[:,0:2], columns=['tsne1', 'tsne2'])
df_tsne["class"] = data_photos["label"]
Images selon les labels
plt.figure(figsize=(10,8))
sns.scatterplot(
x="tsne1", y="tsne2", hue="class", data=df_tsne, legend="brief",
palette=sns.color_palette('tab10', n_colors=5), s=50, alpha=0.6)
plt.title('TSNE selon les vraies classes', fontsize = 20, pad = 30, fontweight = 'bold')
plt.xlabel('tsne1', fontsize = 20, fontweight = 'bold')
plt.ylabel('tsne2', fontsize = 20, fontweight = 'bold')
plt.legend(prop={'size': 14})
plt.show()
Images selon les clusters
cls = cluster.KMeans(n_clusters=4, random_state=6)
cls.fit(feat_pca)
df_tsne["cluster"] = cls.labels_
print(df_tsne.shape)
(616, 4)
plt.figure(figsize=(10,8))
sns.scatterplot(
x="tsne1", y="tsne2",
hue="cluster",
palette=sns.color_palette('tab10', n_colors=4), s=50, alpha=0.6,
data=df_tsne,
legend="brief")
plt.title('TSNE selon les clusters', fontsize = 20, pad = 30, fontweight = 'bold')
plt.xlabel('tsne1', fontsize = 20, fontweight = 'bold')
plt.ylabel('tsne2', fontsize = 20, fontweight = 'bold')
plt.legend(prop={'size': 14})
plt.show()
Calcul ARI
# Chargement du jeu de données
data_photos = joblib.load('data_labels')
# Nouvelle table contenant uniquement les labels encodés
labels = data_photos["labels_bool"]
#data_photos["labels_bool"].unique()
print("ARI : ", metrics.adjusted_rand_score(labels, cls.labels_))
ARI : 0.0002240814777002201
Matrice de confusion
Visualisation de clusters :
# Vue countplot
plt.figure(figsize=(14,6))
sns.countplot(x='cluster', hue='class', data=df_tsne)
plt.title('Visualisation de clusters')
plt.xlabel('Numéro de cluster')
plt.ylabel('Quantité de photos')
plt.show()
# Vue matrix
conf_mat = metrics.confusion_matrix(labels, cls.labels_)
print(conf_mat)
[[121 0 1 1 0] [142 2 0 0 0] [149 2 0 0 0] [ 55 1 0 0 0] [140 2 0 0 0]]
# Réalisation manuellement au lieu d'utiliser la fonction "argmax"
def conf_mat_transform(y_true, y_pred, my_corresp) :
conf_mat = metrics.confusion_matrix(y_true,y_pred)
#corresp = np.argmax(conf_mat, axis=0)
corresp = my_corresp
print ("Correspondance des clusters : ", corresp)
# y_pred_transform = np.apply_along_axis(correspond_fct, 1, y_pred)
labels = pd.Series(y_true, name="y_true").to_frame()
labels['y_pred'] = y_pred
labels['y_pred_transform'] = labels['y_pred'].apply(lambda x : corresp[x])
return labels['y_pred_transform']
my_corresp = [2, 1, 4, 3, 0]
cls_labels_transform = conf_mat_transform(labels, cls.labels_, my_corresp)
conf_mat = metrics.confusion_matrix(labels, cls_labels_transform)
print(conf_mat)
print()
print(metrics.classification_report(labels, cls_labels_transform))
Correspondance des clusters : [2, 1, 4, 3, 0]
[[ 0 0 121 1 1]
[ 0 2 142 0 0]
[ 0 2 149 0 0]
[ 0 1 55 0 0]
[ 0 2 140 0 0]]
precision recall f1-score support
0 0.00 0.00 0.00 123
1 0.29 0.01 0.03 144
2 0.25 0.99 0.39 151
3 0.00 0.00 0.00 56
4 0.00 0.00 0.00 142
accuracy 0.25 616
macro avg 0.11 0.20 0.08 616
weighted avg 0.13 0.25 0.10 616
/home/sylwia/.local/lib/python3.9/site-packages/sklearn/metrics/_classification.py:1318: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior. /home/sylwia/.local/lib/python3.9/site-packages/sklearn/metrics/_classification.py:1318: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior. /home/sylwia/.local/lib/python3.9/site-packages/sklearn/metrics/_classification.py:1318: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples. Use `zero_division` parameter to control this behavior.
df_cm = pd.DataFrame(conf_mat, index = [label for label in list_labels],
columns = [i for i in "01234"])
plt.figure(figsize = (7, 7))
sns.heatmap(df_cm, annot=True, cmap="Greens")
plt.show()
Conclusions
La classification n'est pas parfaite. Seulement 34% d'accuracy pour la totalité des images.
On observe un recall élevé pour une seule catégorie : food - 96 photos sur 123 ont été bien classées. Les autres catégories sont moins bien séparées.
Convolutional Neural Network / ConvNet - les réseaux de neurones convolutifs, sont des modèles les plus performants pour la classification d'images. Leur architecture est composée de deux blocs principaux :
Le Transfer Learning - un transfert de connaissances - permet d'utiliser les connaissances acquises par un réseau de neurones déjà existant.
import tensorflow
from tensorflow import keras
print('TensorFlow version:',tensorflow.__version__)
print('Keras version:',keras.__version__)
TensorFlow version: 2.9.1 Keras version: 2.9.0
L'image à classer avec VGG16 :

# Création du modèle :
model = VGG16(weights='imagenet')
2022-08-23 13:39:14.917652: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcuda.so.1'; dlerror: libcuda.so.1: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/sylwia/anaconda3/envs/nlp/lib/python3.9/site-packages/cv2/../../lib64: 2022-08-23 13:39:14.917690: W tensorflow/stream_executor/cuda/cuda_driver.cc:269] failed call to cuInit: UNKNOWN ERROR (303) 2022-08-23 13:39:14.917715: I tensorflow/stream_executor/cuda/cuda_diagnostics.cc:156] kernel driver does not appear to be running on this host (sylwia-ThinkPad-T460): /proc/driver/nvidia/version does not exist 2022-08-23 13:39:14.918121: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 FMA To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags. 2022-08-23 13:39:15.120326: W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 411041792 exceeds 10% of free system memory. 2022-08-23 13:39:15.476882: W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 411041792 exceeds 10% of free system memory. 2022-08-23 13:39:15.598775: W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 411041792 exceeds 10% of free system memory. 2022-08-23 13:39:17.614911: W tensorflow/core/framework/cpu_allocator_impl.cc:82] Allocation of 411041792 exceeds 10% of free system memory.
# Chargement et définition de la taille
img = load_img('png/resto.jpg', target_size=(224, 224))
# Conversion en tableau numpy
img = img_to_array(img)
# Ajout dela 4ème dimension
img = img.reshape((1, img.shape[0], img.shape[1], img.shape[2]))
# Prétraitement
img = preprocess_input(img)
# Prédire la classe de l'image (parmi les 1000 classes d'ImageNet)
y = model.predict(img)
1/1 [==============================] - 0s 497ms/step
Le résultat est une liste de classes et leurs probabilités.
# Afficher les 3 classes les plus probables
print('Top 3 :', decode_predictions(y, top=3)[0])
Top 3 : [('n03032252', 'cinema', 0.80120504), ('n04081281', 'restaurant', 0.038507678), ('n03788195', 'mosque', 0.019463148)]
Notre image a été identifiée en tant que cinéma à 80% et en tant que restaurant à seulement 3%.
Le réseau VGG16 a été pré-entraîné sur un problème de classification à 1000 classes.
Notre besoin est diffférent : nous sommes confrontés à un problème de classification à 5 catégories. Nous allons faire appel au Transfer Learning. L'objectif est de remplacer les dernières couches fully-connected qui permettent de ranger l'image dans une des 1000 classes par un classifieur plus adapté au problème.
# Chargement du jeu de données
features_vgg_df = joblib.load('features_vgg_df')
PCA
variance = 0.99
pca = PCA(variance)
pca.fit(features_vgg_df)
feat_vgg_pca = pca.transform(features_vgg_df)
print('Dimensions dataset avant réduction PCA : ' + str(features_vgg_df.shape[1]))
print('Dimensions dataset après réduction PCA : ' + str(pca.n_components_))
Dimensions dataset avant réduction PCA : 4096 Dimensions dataset après réduction PCA : 542
TSNE
tsne = manifold.TSNE(n_components=2, perplexity=30,
n_iter=2000, init='random', random_state=6)
X_tsne = tsne.fit_transform(feat_vgg_pca)
/home/sylwia/.local/lib/python3.9/site-packages/sklearn/manifold/_t_sne.py:790: FutureWarning: The default learning rate in TSNE will change from 200.0 to 'auto' in 1.2.
# Création d'un nouveau dataframe
df_tsne = pd.DataFrame(X_tsne[:,0:2], columns=['tsne1', 'tsne2'])
df_tsne["class"] = data_photos["label"]
Images selon les labels
plt.figure(figsize=(10,8))
sns.scatterplot(
x="tsne1", y="tsne2", hue="class", data=df_tsne, legend="brief",
palette=sns.color_palette('tab10', n_colors=5), s=50, alpha=0.6)
plt.title('TSNE selon les vraies classes', fontsize = 20, pad = 30, fontweight = 'bold')
plt.xlabel('tsne1', fontsize = 20, fontweight = 'bold')
plt.ylabel('tsne2', fontsize = 20, fontweight = 'bold')
plt.legend(prop={'size': 14})
plt.show()
Images selon les clusters
cls = cluster.KMeans(n_clusters=5, random_state=6)
cls.fit(feat_vgg_pca)
KMeans(n_clusters=5, random_state=6)
df_tsne["cluster"] = cls.labels_
print(df_tsne.shape)
(616, 4)
plt.figure(figsize=(10,8))
sns.scatterplot(
x="tsne1", y="tsne2",
hue="cluster",
palette=sns.color_palette('tab10', n_colors=5), s=50, alpha=0.6,
data=df_tsne,
legend="brief")
plt.title('TSNE selon les clusters', fontsize = 20, pad = 30, fontweight = 'bold')
plt.xlabel('tsne1', fontsize = 20, fontweight = 'bold')
plt.ylabel('tsne2', fontsize = 20, fontweight = 'bold')
plt.legend(prop={'size': 14})
plt.show()
# Clustering avec KMeans
kmeansVGG = KMeans(n_clusters=len(list_labels), random_state=3)
kmeansVGG.fit(feat_vgg_pca)
KMeans(n_clusters=5, random_state=3)
# Stockage des noms de photos dans une liste
liste_noms_image = []
for i, row in data_photos.iterrows():
nom = PATH + data_photos.photo_id[i]
liste_noms_image.append(nom)
liste_noms_image = [name.split('/')[-1] for name in liste_noms_image]
# Stockage de photos au niveau des clusters respectifs
groups = {}
for file, cluster in zip(liste_noms_image, kmeansVGG.labels_):
if cluster not in groups.keys():
groups[cluster] = []
groups[cluster].append(file)
else:
groups[cluster].append(file)
# Stockage des résultats dans un dataframe
cluster_groups = pd.DataFrame(groups.items(), columns=['Cluster', 'Image'])
#cluster_groups.head()
# Fonction permettant d'afficher les photos appartenant à un cluster donné
def view_cluster(cluster):
plt.figure(figsize=(25, 25))
# gets the list of filenames for a cluster
files = groups[cluster]
# only allow up to 15 images to be shown at a time
if len(files) > 30:
print(f"Clipping cluster size from {len(files)} to 30")
files = files[:29]
# plot each image in the cluster
for index, file in enumerate(files):
plt.subplot(8, 8, index+1)
img = load_img(PATH + file)
img = np.array(img)
plt.imshow(img)
plt.axis('off')
view_cluster(1)
Clipping cluster size from 144 to 30
Ce cluster semble correspondre à la classe "outside".
view_cluster(2)
Clipping cluster size from 137 to 30
Ce cluster correspond à la classe "drink".
view_cluster(3)
Clipping cluster size from 54 to 30
Ce cluster correspond à la classe "menu".
view_cluster(4)
Clipping cluster size from 147 to 30
Ce cluster correspond clairement à la classe "inside".
view_cluster(0)
Clipping cluster size from 134 to 30
Ce cluster correspond au label food.
Calcul ARI
# Calcul ARI
print("ARI : ", metrics.adjusted_rand_score(labels, cls.labels_))
ARI : 0.6749992777265746
Matrice de confusion
# Visualisation des résultats avec un countplot
plt.figure(figsize=(14,6))
sns.countplot(x='cluster', hue='class', data=df_tsne)
plt.title('Visualisation de clusters')
plt.xlabel('Numéro de cluster')
plt.ylabel('Quantité de photos')
plt.show()
# Vue matrix
conf_mat = metrics.confusion_matrix(labels, cls.labels_)
my_corresp = [1, 2, 4, 3, 0]
cls_labels_transform = conf_mat_transform(labels, cls.labels_, my_corresp)
conf_mat = metrics.confusion_matrix(labels, cls_labels_transform)
print(conf_mat)
print()
print(metrics.classification_report(labels, cls_labels_transform))
Correspondance des clusters : [1, 2, 4, 3, 0]
[[115 2 5 1 0]
[ 20 0 122 1 1]
[ 12 11 5 0 123]
[ 0 5 1 48 2]
[ 3 122 0 1 16]]
precision recall f1-score support
0 0.77 0.93 0.84 123
1 0.00 0.00 0.00 144
2 0.04 0.03 0.04 151
3 0.94 0.86 0.90 56
4 0.11 0.11 0.11 142
accuracy 0.30 616
macro avg 0.37 0.39 0.38 616
weighted avg 0.27 0.30 0.28 616
df_cm = pd.DataFrame(conf_mat, index = [label for label in list_labels],
columns = [i for i in "01234"])
plt.figure(figsize = (7, 7))
sns.heatmap(df_cm, annot=True, cmap="Oranges")
plt.show()
Conclusions
Les résultats sont très satisfaisants. L'algorithme a bien classé les images de toutes les catégories. Les photos les moins bien séparées appartiennent au label outside - seulement 51 photos reconnues sur 123.
La classification avec VGG16 est de loin meilleure avec 83% d'accuracy pour l'ensemble des catégories.
Dans un souci de valider la faisabilité de la solution, nous allons collecter de nouvelles données via l'API YELP.
# Chargement du jeu de données
df_reviews_api = joblib.load('df_reviews_api')
df_reviews_api.head()
| review_id | business_id | stars | text | |
|---|---|---|---|---|
| 0 | gKnt3x8FFTduKx_UWakMVA | V7lXZKBDzScDeGB8JmnzSA | 3 | When you look up places to eat in New York Cit... |
| 1 | wIT-ElLs-ryrdUSyQrXRsw | V7lXZKBDzScDeGB8JmnzSA | 5 | No word can describe \nVery nice and tender \n... |
| 2 | Z70us1M9d-pbwAxkbT4C4A | V7lXZKBDzScDeGB8JmnzSA | 5 | Excellent, GIANT corned beef sandwiches!! Kat... |
| 3 | TD9YpWt29UXU_JnpyGQgng | 44SY464xDHbvOcjDzRbKkQ | 5 | One of my favorite ramen places I have been. \... |
| 4 | 7__yw0Eb3hVRpjD2MgWR-A | 44SY464xDHbvOcjDzRbKkQ | 3 | Went here with friends for dinner.\nThe overal... |
# Chargement du jeu de données
df_photos_api = joblib.load('df_photos_api')
df_photos_api.head()
| photo_url | business_id | |
|---|---|---|
| 0 | https://s3-media1.fl.yelpcdn.com/bphoto/mrIdx2... | V7lXZKBDzScDeGB8JmnzSA |
| 1 | https://s3-media1.fl.yelpcdn.com/bphoto/zF3Egq... | 44SY464xDHbvOcjDzRbKkQ |
| 2 | https://s3-media1.fl.yelpcdn.com/bphoto/MYnXpr... | xEnNFXtMLDF5kZDxfaCJgA |
| 3 | https://s3-media4.fl.yelpcdn.com/bphoto/xM4eGR... | 0CjK3esfpFcxIopebzjFxA |
| 4 | https://s3-media1.fl.yelpcdn.com/bphoto/d0XSKE... | 4yPqqJDJOQX69gC66YUDkA |
from skimage.io import imshow, imread
def show_img_api(new_img) :
plt.figure(figsize=(10, 8))
imshow(new_img)
plt.axis('off')
plt.title("Image via l'API Yelp")
plt.show()
api_img = df_photos_api.photo_url[4]
show_img_api(api_img)
api_img = df_photos_api.photo_url[122]
show_img_api(api_img)